package com.deepgames.mud.session;

import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.deepgames.mud.core.InternalCode;
import com.deepgames.mud.core.Message;

/**
 * Manages a collection of active telnet connections.  Applications can send
 * and receive messages from this manager using Player IDs as session keys.
 * 
 * @author Zack Hoffmann
 * @version 0.1
 * 
 */
public class SessionManager implements Runnable {

	private SessionServer server;
	private Map<String, Session> sessionMap;
	private List<Session> sessionList;
	private Queue<Message> incomingMessages;
	private Logger log;
	
	/**
	 * Constructor designating the port to listen for new connections on.
	 * 
	 * @throws IOException 
	 */
	public SessionManager() throws IOException {
		log = Logger.getLogger(SessionManager.class.getName());
		log.setLevel(Level.parse(System.getProperty("deepmud.log.level")));
		sessionMap = new HashMap<String, Session>();
		incomingMessages = new ConcurrentLinkedQueue<Message>();
		sessionList = new ArrayList<Session>();
		this.server = new SessionServer(Integer.parseInt(System.getProperty("deepmud.server.port")), this);
	}
	
	/**
	 * Waits for socket connections and maintains the list of active sessions.
	 */
	@Override
	public void run() {
		Session newSession;
		Session closed;
		String guestID = null;
		Random randID = new Random();
		DecimalFormat df = new DecimalFormat("0000");
		
		// Try guest IDs at random until a vacant one is found.
		/*
		 * NOTE: This is an arbitrary range, but collisions should be
		 * 	infrequent for the scope of this application.  Might be worth
		 * 	while to implement guest session timeouts in the future to
		 * 	ensure that these IDs get recycled.
		 */
		while (guestID == null) {
			guestID = "GUEST_" + df.format(randID.nextInt(9999));
			if (sessionMap.get(guestID) != null)
				guestID = null;
		}
		
		// Accept a new session and assign it a player ID.
		try {
			newSession = server.accept();
			newSession.setPlayerID(guestID);
			sessionMap.put(guestID, newSession);
			sessionList.add(newSession);
			new Thread(newSession).start();
			this.sendMsgToGame(guestID, "LOGIN");
		} catch (IOException e) {
			log.severe("Attempt to accept client connection failed.");
			log.severe(e.toString());
			e.printStackTrace();
		}

		// Clean up closed sessions.
		for (int i = 0; i < sessionList.size(); i++) {
			closed = sessionList.get(i);
			if (closed.isClosed()) {
				sessionList.remove(i);
				sessionMap.remove(closed.getPlayerID());
			}
		}
		
	}
	
	/**
	 * Sends a message to a session according to Player ID.
	 * 
	 * @param playerID		Unique ID of the player to send the message to.
	 * @param msg			Message to send.
	 * @return				Status code.  {@link InternalCode}
	 */
	public int sendMsgToPlayer(String playerID, String msg) {
		int status = InternalCode.FAILURE;
		sessionMap.get(playerID).sendMsg(msg);
		return status;
	}
	
	/**
	 * Sends a message to the game's incoming message queue.  This method
	 * 	should only be called by an active session.
	 * 
	 * @param playerID		Unique ID of the player the message is being sent
	 * 						from.
	 * @param msg			Message being sent by the player.
	 * @return				Status code.  {@link InternalCode}
	 */
	public int sendMsgToGame(String playerID, String msg) {
		int status = InternalCode.SUCCESS;
		incomingMessages.add(new Message(playerID, msg));
		return status;
	}
	
	/**
	 * Pops the next message off of the game's incoming message queue.
	 * 
	 * @return				The next message from the game's incoming message
	 * 						queue.
	 */
	public Message getNextMsgFromPlayers() {
		Message msg = null;
		msg = incomingMessages.poll();
		return msg;
	}
	
	/**
	 * Designates a new Player ID for the session matched by the previous ID.
	 * 	The new Player ID should have previously been asserted as unique.
	 * 
	 * @param oldPlayerID	Unique ID of the player whose ID should be changed.
	 * @param newPlayerID	New unique ID to assign to the player.
	 * @return				Status code.  {@link InternalCode}
	 */
	public int setNewPlayerID(String oldPlayerID, String newPlayerID) {
		int status = InternalCode.SUCCESS;
		sessionMap.get(oldPlayerID).setPlayerID(newPlayerID);
		sessionMap.put(newPlayerID, sessionMap.get(newPlayerID));
		sessionMap.remove(oldPlayerID);
		return status;
	}

}
